package com.simplechain.link;

import com.simplechain.constant.TransactionConstant;
import com.simplechain.constant.TransactionConstant.Event;
import com.simplechain.constant.TransactionConstant.Result;
import com.simplechain.contract.ContractCall;
import com.simplechain.util.Utils;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;

import link.luyu.protocol.algorithm.ecdsa.secp256k1.SignatureData;
import link.luyu.protocol.common.STATUS;
import link.luyu.protocol.link.Connection;
import link.luyu.protocol.network.Block;
import link.luyu.toolkit.abi.ContractABI;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.Sign;
import org.web3j.crypto.transaction.type.TransactionType;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.DefaultBlockParameterNumber;
import org.web3j.protocol.core.Request;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.rlp.RlpEncoder;
import org.web3j.rlp.RlpList;
import org.web3j.rlp.RlpType;
import org.web3j.utils.Numeric;

public class SipcConnection implements Connection {
    private Map<Integer, RawTransaction> rawTransactionMap = new HashMap<>();
    private Web3j web3j;
    private String chainPath;
    String contractManagerAddr = "";

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();


    public void start() throws RuntimeException {
    }

    public void stop() throws RuntimeException {
    }

    public SipcConnection(Map<String, Object> properties) throws Exception {
        Map<String, Object> common = (Map<String, Object>) properties.get("common");
        if (common == null) {
            throw new Exception("[common] item not found");
        }

        //连接链
        String chainUrl = (String) common.get("chainUrl");
        if (chainUrl == null) {
            throw new Exception("\"chainUrl\" item not found");
        }
        this.web3j = Web3j.build(new org.web3j.protocol.http.HttpService((chainUrl)));

        //设置本地连接的chainPath
        chainPath = (String) properties.get("chainPath");


        //获取本地链的的资源管理合约地址
        String proxyContract = (String) common.get("proxyContract");
        if (proxyContract == null) {
            throw new Exception(chainPath + "： [proxyContract] item not found");
        }
        contractManagerAddr = proxyContract;

    }

    @Override
    public void asyncSend(String path, int type, byte[] data, Callback callback) {
        ObjectMapper objectMapper = new ObjectMapper();
        switch (type) {
            case TransactionConstant.Type.SEND_TRANSACTION: {
                if (path.equals("1")) {
                    try {
                        TransactionDataVo transactionDataVo =
                                OBJECT_MAPPER.readValue(data, TransactionDataVo.class);

                        String address = transactionDataVo.address;
                        String contract = transactionDataVo.contract;
                        String data2 = transactionDataVo.data;

                        //获取account的nonce
                        EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount(
                                address, DefaultBlockParameterName.PENDING).sendAsync().get();

                        BigInteger nonce = ethGetTransactionCount.getTransactionCount();

                        // using a raw transaction
                        RawTransaction rawTransaction = RawTransaction.createTransaction(
                                nonce,
                                new BigInteger("0"),
                                new BigInteger("1000000"),
                                contract,
                                new BigInteger("0"),
                                data2);

                        byte[] encodedTransaction = org.web3j.crypto.TransactionEncoder.encode(rawTransaction);

                        RawTransactionVo rawTransactionVo = new RawTransactionVo();
                        rawTransactionVo.encodedTransaction = encodedTransaction;
                        rawTransactionVo.rawTransactionHashcode = rawTransaction.hashCode();
                        rawTransactionMap.put(rawTransaction.hashCode(), rawTransaction);

                        byte[] rawTransactionVoBytes = OBJECT_MAPPER.writeValueAsBytes(rawTransactionVo);
                        callback.onResponse(
                                Result.SUCCESS, null, rawTransactionVoBytes);

                    } catch (Exception e) {
                        callback.onResponse(
                                Result.ERROR, "transaction failed,error" + e.getMessage(), null);
                    }
                } else if (path.equals("2")) {
                    try {
                        RawTransactionVo rawTransactionVo = null;
                        try {
                            rawTransactionVo = OBJECT_MAPPER.readValue(data, RawTransactionVo.class);
                        } catch (IOException e) {
                            e.printStackTrace();

                        }

                        if (rawTransactionVo == null) {
                            callback.onResponse(STATUS.INTERNAL_ERROR, "create rawTransactionVo object error", null);
                            return;
                        }

                        //account签名
                        SignatureData luyuSignData = SignatureData.parseFrom(rawTransactionVo.luyuSignBytes);

                        Integer rawTransactionHashcode = rawTransactionVo.rawTransactionHashcode;

                        if (!rawTransactionMap.containsKey(rawTransactionHashcode)) {
                            callback.onResponse(STATUS.INTERNAL_ERROR, "rawTransaction object missed!!", null);
                            return;
                        }

                        RawTransaction rawTransaction = rawTransactionMap.get(rawTransactionHashcode);
                        rawTransactionMap.remove(rawTransactionHashcode);

                        //编码
                        byte[] signedMessage = encode(rawTransaction, new Sign.SignatureData(
                                (byte) luyuSignData.getV(),
                                luyuSignData.getR().toByteArray(),
                                luyuSignData.getS().toByteArray()));

                        String hexValue = Numeric.toHexString(signedMessage);


                        // send...
                        EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
                        String transactionHash = ethSendTransaction.getTransactionHash();
                        callback.onResponse(Result.SUCCESS, "Success", transactionHash.getBytes(StandardCharsets.UTF_8));
                    } catch (Exception e) {
                        callback.onResponse(
                                Result.ERROR, "transaction failed,error" + e.getMessage(), null);
                    }
                } else {
                    callback.onResponse(
                            STATUS.CONNECTION_EXCEPTION, "plugin connection error ,need check.", null);
                    return;
                }
                break;
            }

            case TransactionConstant.Type.CALL_TRANSACTION: {
                try {
                    ContractCall contractCall =
                            OBJECT_MAPPER.readValue(data, ContractCall.class);
                    org.web3j.protocol.core.methods.request.Transaction ethCallTransaction =
                            org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction(
                                    contractCall.getSender(), contractCall.getContract(), contractCall.getData());

                    String result = web3j.ethCall(ethCallTransaction, DefaultBlockParameterName.PENDING).send().getValue();
                    callback.onResponse(Result.SUCCESS, "Success", result.getBytes());
                } catch (IOException e) {
                    callback.onResponse(
                            Result.ERROR, "call failed,error" + e.getMessage(), null);
                }
                break;
            }

            case TransactionConstant.Type.GET_TRANSACTION_RECEIPT: {
                try {
                    EthGetTransactionReceipt receipt =
                            web3j.ethGetTransactionReceipt(new String(data)).send();
                    if (receipt.hasError()) {
                        callback.onResponse(
                                TransactionConstant.Result.ERROR,
                                receipt.getRawResponse(),
                                null);
                        return;
                    }
                    callback.onResponse(
                            TransactionConstant.Result.SUCCESS,
                            "Success",
                            OBJECT_MAPPER.writeValueAsBytes(receipt.getResult()));
                } catch (Exception e) {
                    callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                }
                break;


            }

            case TransactionConstant.Type.GET_ABI: {
                try {
                    String contractManagerAbi = "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"acceptAsTheManager\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_addr\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"_contractName\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractAbi\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractOwner\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractIntroduce\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractVersion\",\"type\":\"string\"}],\"name\":\"addContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"beAuthorizedNewManager\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"findContract\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAddressAll\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getContractAll\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_newManager\",\"type\":\"address\"}],\"name\":\"grantManager\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"manager\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"removeContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]";

                    String abi = contractManagerAbi;     //管理合约abi
                    String contract = path;     //待查找的合约资源地址

                    ContractABI ctAbi = new ContractABI(abi);
                    try {
                        ctAbi.getFunctions("findContract").get(0);
                    } catch (IndexOutOfBoundsException ioe) {
                        callback.onResponse(
                                STATUS.INTERNAL_ERROR,
                                "this contractManager no method found, check contractManagerAddress.",
                                null);
                        return;
                    }

//                    String[] args = {contract};
                    byte[] pubKey = data;
                    String sender = Utils.publicKeyToEthAddress(pubKey);


                    String methodName = "findContract";

                    List<Type> inputParameters = new ArrayList<>();
                    inputParameters.add(new Address(contract));

                    List<TypeReference<?>> outputs = new ArrayList<>();
                    TypeReference<Utf8String> typeReference = new TypeReference<Utf8String>() {
                    };
                    outputs.add(typeReference);

                    Function function = new Function(methodName, inputParameters, outputs);
                    String data2 = FunctionEncoder.encode(function);
                    org.web3j.protocol.core.methods.request.Transaction ethCallTransaction =
                            org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction(
                                    sender, contractManagerAddr, data2);

                    String result = web3j.ethCall(ethCallTransaction, DefaultBlockParameterName.PENDING).send().getValue();


                    //将返回结果转化为合约详情
                    ContractVo contractInfo = null;
                    String contractInfoStr = Utils.hexStr2Str(result);
                    contractInfoStr = contractInfoStr.substring(contractInfoStr.indexOf('{'), contractInfoStr.indexOf('}') + 1);

                    try {
                        contractInfo = objectMapper.readValue(contractInfoStr, ContractVo.class);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    String resultAbi = Utils.hexStr2Str(contractInfo.contractAbi);

                    callback.onResponse(Result.SUCCESS, "Success", resultAbi.getBytes());

                } catch (IOException e) {
                    callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                }
                break;
            }

            case TransactionConstant.Type.GET_ContractAddressAll: {
                try {
                    String contractManagerAbi = "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"acceptAsTheManager\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_addr\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"_contractName\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractAbi\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractOwner\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractIntroduce\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_contractVersion\",\"type\":\"string\"}],\"name\":\"addContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"beAuthorizedNewManager\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"findContract\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAddressAll\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getContractAll\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_newManager\",\"type\":\"address\"}],\"name\":\"grantManager\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"manager\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"removeContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]";

                    String abi = contractManagerAbi;     //管理合约abi

                    ContractABI ctAbi = new ContractABI(abi);
                    try {
                        ctAbi.getFunctions("findContract").get(0);
                    } catch (IndexOutOfBoundsException ioe) {
                        callback.onResponse(
                                STATUS.INTERNAL_ERROR,
                                "this contractManager no method found, check contractManagerAddress.",
                                null);
                        return;
                    }

//                    String[] args = {};
//                    Function function = Utils.convertFunction(abi, "getAddressAll", args);

                    String methodName = "getAddressAll";

                    List<Type> inputParameters = new ArrayList<>();

                    List<TypeReference<?>> outputs = new ArrayList<>();
                    TypeReference<Utf8String> typeReference = new TypeReference<Utf8String>() {
                    };
                    outputs.add(typeReference);

                    Function function = new Function(methodName, inputParameters, outputs);
                    String data2 = FunctionEncoder.encode(function);

                    org.web3j.protocol.core.methods.request.Transaction ethCallTransaction =
                            org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction(
                                    "0x9a41046Ea7417926391449cb1Db185e949A49cD6", contractManagerAddr, data2);

                    String result = web3j.ethCall(ethCallTransaction, DefaultBlockParameterName.PENDING).send().getValue();

                    String contractAddrAllStr = Utils.hexStr2Str(result);
                    contractAddrAllStr = contractAddrAllStr.substring(contractAddrAllStr.indexOf('[') + 1, contractAddrAllStr.indexOf(']'));
                    String[] addrAll = contractAddrAllStr.split(",");

                    ArrayList<String> resourcePathList = new ArrayList<String>();

                    for (String addr : addrAll) {
                        String resourcePath = this.chainPath + "." + addr.trim();
                        resourcePathList.add(resourcePath);
                    }

                    byte[] bytes = objectMapper.writeValueAsBytes(resourcePathList);
                    callback.onResponse(Result.SUCCESS, "Success", bytes);

                } catch (IOException e) {
                    callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                }
                break;
            }

            case TransactionConstant.Type.GET_BLOCK_NUMBER: {
                try {
                    EthBlockNumber ret = web3j.ethBlockNumber().send();
                    long blockNumber = ret.getBlockNumber().longValue();
                    callback.onResponse(
                            TransactionConstant.Result.SUCCESS,
                            "Success",
                            Utils.longToBytes(blockNumber));
                } catch (IOException e) {
                    callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                }
                break;
            }

            case TransactionConstant.Type.GET_BLOCK_BY_NUMBER: {
                long blockNumber = Utils.bytesToLong(data);
                try {
                    Request<?, EthBlock> ethBlockRequest = web3j.ethGetBlockByNumber(new DefaultBlockParameterNumber(blockNumber), false);

//                    com.simplechain.util.Utils.saveFile("", "GET_BLOCK_BY_NUMBER(0) => Start..", false);
                    EthBlock ethBlock = ethBlockRequest.send();
                    if (ethBlock == null || ethBlock.getBlock() == null) {
                        callback.onResponse(STATUS.INTERNAL_ERROR, "Block is empty", null);
                        return;
                    }

                    Block luyuBlock = new Block();
                    EthBlock.Block block = ethBlock.getBlock();
                    luyuBlock.setNumber(block.getNumber().longValue());
                    luyuBlock.setHash(block.getHash());
                    luyuBlock.setTimestamp(block.getTimestamp().longValue());
                    luyuBlock.setParentHash(new String[]{block.getParentHash()});
                    luyuBlock.setRoots(new String[]{block.getTransactionsRoot(), block.getStateRoot(), block.getReceiptsRoot()});

                    List<String> transactionHashs = new ArrayList<>();
                    for (EthBlock.TransactionResult transaction : block.getTransactions()) {
                        transactionHashs.add(((Transaction) transaction.get()).getHash());
                    }
                    luyuBlock.setTransactionHashs(transactionHashs.toArray(new String[transactionHashs.size()]));
                    luyuBlock.setBytes(block.getExtraData().getBytes(StandardCharsets.UTF_8));
//
                    callback.onResponse(STATUS.OK, "Success", objectMapper.writeValueAsBytes(luyuBlock));
                } catch (IOException e) {
                    callback.onResponse(STATUS.CONNECTION_EXCEPTION, e.getMessage(), null);
                }
                break;
            }

            case TransactionConstant.Type.GET_BLOCK_BY_HASH: {
                String blockHash = path;
                try {
                    EthBlock ethBlock = web3j.ethGetBlockByHash(blockHash, false).send();
                    if (ethBlock == null || ethBlock.getBlock() == null) {
                        callback.onResponse(STATUS.INTERNAL_ERROR, "Block is empty", null);
                        return;
                    }

                    Block luyuBlock = new Block();
                    EthBlock.Block block = ethBlock.getBlock();
                    luyuBlock.setNumber(block.getNumber().longValue());
                    luyuBlock.setHash(block.getHash());
                    luyuBlock.setTimestamp(block.getTimestamp().longValue());
                    luyuBlock.setParentHash(new String[]{block.getParentHash()});
                    luyuBlock.setRoots(new String[]{block.getTransactionsRoot(), block.getStateRoot(), block.getReceiptsRoot()});

                    List<String> transactionHashs = new ArrayList<>();
                    for (EthBlock.TransactionResult transaction : block.getTransactions()) {
                        transactionHashs.add(((Transaction) transaction.get()).getHash());
                    }
                    luyuBlock.setTransactionHashs(transactionHashs.toArray(new String[transactionHashs.size()]));
                    luyuBlock.setBytes(block.getExtraData().getBytes(StandardCharsets.UTF_8));

//                    com.simplechain.util.Utils.saveFile("", "getBlockByHash:" + luyuBlock, true);

                    callback.onResponse(STATUS.OK, "Success", objectMapper.writeValueAsBytes(luyuBlock));
                } catch (IOException e) {
                    callback.onResponse(STATUS.CONNECTION_EXCEPTION, e.getMessage(), null);
                }
                break;
            }

            default: {
                callback.onResponse(
                        TransactionConstant.Result.ERROR, "Unrecognized type of " + type, null);
                break;
            }
        }
    }

    @Override
    public void subscribe(int type, byte[] data, Callback callback) {
        switch (type) {
            case Event.EVENT_NEW_BLOCK: {
                break;
            }

            case Event.EVENT_RESOURCES_CHANGED: {
                break;
            }
            default:
        }
    }

    private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) {
        List<RlpType> values = rawTransaction.getTransaction().asRlpValues(signatureData);
        RlpList rlpList = new RlpList(values);
        byte[] encoded = RlpEncoder.encode(rlpList);
        if (!rawTransaction.getType().equals(TransactionType.LEGACY)) {
            return ByteBuffer.allocate(encoded.length + 1)
                    .put(rawTransaction.getType().getRlpType())
                    .put(encoded)
                    .array();
        }
        return encoded;
    }
}
